from google.appengine.api.urlfetch import fetch as urlfetch
from google.appengine.ext import db
from sessionhandling.sessioncookie import SessionCookie

class OAuthClient(object):

    __public__ = ('callback', 'cleanup', 'login', 'logout')

    def __init__(self, service, handler, oauth_callback=None, **request_params):
        from oauth.config import OAUTH_APP_SETTINGS
        self.service = service
        self.service_info = OAUTH_APP_SETTINGS[service]
        self.service_key = None
        self.handler = handler
        self.request_params = request_params
        self.oauth_callback = oauth_callback
        self.response_headers = None
        self.token = None
    
    def get(self, api_method, http_method='GET', expected_status=(200,), **extra_params):
        from oauth.tokens import OAuthAccessToken
        
        if not (api_method.startswith('http://') or api_method.startswith('https://')):
            api_method = '%s%s%s' % (
                self.service_info['default_api_prefix'], api_method,
                self.service_info['json_api_suffix']
                )

        if self.token is None:
            self.token = OAuthAccessToken.get_by_key_name(self.get_cookie())
        
        url, header = self.get_signed_url(api_method, self.token, http_method, **extra_params)
        
        fetch = urlfetch(url, headers={'Authorization': header}, deadline=10)
        
        if fetch.status_code not in expected_status:
            raise ValueError(
                "Error calling... Got return status: %i [%r]" %
                (fetch.status_code, fetch.content)
                )
        
        return fetch.content
    
    def post(self, api_method, http_method='POST', expected_status=(200,), **extra_params):
        from oauth.tokens import OAuthAccessToken
        
        if not (api_method.startswith('http://') or api_method.startswith('https://')):
            api_method = '%s%s%s' % (
                self.service_info['default_api_prefix'], api_method,
                self.service_info['json_api_suffix']
                )

        if self.token is None:
            self.token = OAuthAccessToken.get_by_key_name(self.get_cookie())
        
        signed_body, header = self.get_signed_body(api_method, self.token, http_method, **extra_params)
        
        fetch = urlfetch(url=api_method, payload=signed_body, headers={'Authorization': header},  method=http_method, deadline=10)

        if fetch.status_code not in expected_status:
            raise ValueError(
                "Error calling... Got return status: %i [%r]" %
                (fetch.status_code, fetch.content)
                )
        
        return fetch.content
    
    def get_document(self, api_method, http_method='GET', expected_status=(200,), **extra_params):
        from oauth.tokens import OAuthAccessToken
        
        if not (api_method.startswith('http://') or api_method.startswith('https://')):
            api_method = '%s%s' % (
                self.service_info['default_api_prefix'], api_method
                )

        if self.token is None:
            self.token = OAuthAccessToken.get_by_key_name(self.get_cookie())
        
        url, header = self.get_signed_url(api_method, self.token, http_method, **extra_params)
        
        fetch = urlfetch(url, headers={'Authorization': header}, deadline=10)
        
        if fetch.status_code not in expected_status:
            raise ValueError(
                "Error calling... Got return status: %i [%r]" %
                (fetch.status_code, fetch.content)
                )
        self.response_headers = fetch.headers
        return fetch.content
    
    def get_xml(self, api_method, http_method='GET', expected_status=(200,), **extra_params):
        from oauth.tokens import OAuthAccessToken
        from time import time
        
        if not (api_method.startswith('http://') or api_method.startswith('https://')):
            api_method = '%s%s%s' % (
                self.service_info['default_api_prefix'], api_method,
                self.service_info['xml_api_suffix']
                )

        if self.token is None:
            self.token = OAuthAccessToken.get_by_key_name(self.get_cookie())

        url, header = self.get_signed_url(api_method, self.token, http_method, **extra_params)
        
        fetch = urlfetch(url, headers={'Authorization': header}, deadline=10)
        
        if fetch.status_code not in expected_status:
            raise ValueError(
                "Error calling... Got return status: %i [%r]" %
                (fetch.status_code, fetch.content)
                )
        self.response_headers = fetch.headers
        return fetch.content.decode("utf8")

    def get_request_token(self):
        """
        Initiates the OAuth flow by first calling the request token URL specified in
        oauth.config.OAUTH_APP_SETTINGS.
        
        After that it sees to it that the client is redirected to the authorization page.
        """
        from oauth.tokens import OAuthRequestToken
        
        token_info = self.get_data_from_signed_url(
            self.service_info['request_token_url'], **self.request_params
            )
        
        
        token = OAuthRequestToken(
            service=self.service,
            **dict(token.split('=') for token in token_info.split('&'))
            )

        token.put()

        if self.oauth_callback:
            oauth_callback = {'oauth_callback': self.oauth_callback}
        else:
            oauth_callback = {}

        self.handler.redirect(self.get_signed_url(self.service_info['user_auth_url'], token, **oauth_callback)[0])

    def get_data_from_signed_url(self, url, token=None, method='GET', **extra_params):
        """
        Returns the response body of a request. 
        
        @param url: the URL
        @type url: str
        
        @param token: May be an access or request token
        @type token: oauth.tokens.OAuthRequestToken | oauth.tokens.OAuthAccessToken
        
        @param method: The http method used, defaults to get
        @type method: "POST" | "GET"
        
        @param **extra_params: Any extra parameters that should go with the request. Optional.
        @type **extra_params: {'param1': 'value1', ...}
        
        @return: A string with the response body
        @returntype: str
        """
        url, header = self.get_signed_url(url, token, method, **extra_params)
        
        result = urlfetch(url, headers={'Authorization' : header}, deadline=10).content
        
        return result
            
    def get_signed_url(self, url, token=None, method='GET', **extra_params):
        """
        Returns an URL with potential API parameters appended. And an OAuth header.
        
        @param url: the URL
        @type url: str
        
        @param token: May be an access or request token
        @type token: oauth.tokens.OAuthRequestToken | oauth.tokens.OAuthAccessToken
        
        @param method: The http method used, defaults to get
        @type method: "POST" | "GET"
        
        @param **extra_params: Any extra parameters that should go with the request. Optional.
        @type **extra_params: {'param1': 'value1', ...}
        
        @return: A tuple, where the first value is the url that the OAuth request should be sent to.
                 The second value is the OAuth authorization header.
                 as such:
                 ('https://....?param1=value1...etc',
                 'Oauth oauth_nonce="1232123",...etc')
        @returntype: (str, str)
        """
        
        signed_body, oauth_header = self.get_signed_body(url, token, method, **extra_params)
        url = url
        
        if signed_body:
            url += "?%s" % signed_body
        
        return url, oauth_header

    def get_signed_body(self, url, token=None, method='GET', **extra_params):
        """
        Returns a request body (i.e a urlencoded parameter list) which excludes oauth parameters.
        Also returns the OAuth-header to be used with this request body.
        
        @param url: the URL
        @type url: str
        
        @param token: May be an access or a request token
        @type token: oauth.tokens.OAuthRequestToken | oauth.tokens.OAuthAccessToken
        
        @param method: The http method used, defaults to get
        @type method: "POST" | "GET"
        
        @param **extra_params: Any extra parameters that should go with the request. Optional.
        @type **extra_params: {'param1': 'value1', ...}
        
        @return: A tuple, where the first value is the url encoded parameter list, and the second the value that
                 should go in the request's Authorization header. As such:
                 ('parameter1=value1',
                 'Oauth oauth_nonce="1232123",...etc')
        @returntype: (str, str)
        
        @note: This function is the real "engine" in the OAuth implementation.       
        """
        from urllib import urlencode
        
        parameters = self.get_params(extra_params, token)
        
        parameters['oauth_signature'] = self.construct_signature(url, method, parameters, token)
        
        if not url == self.service_info['user_auth_url']:
            oauth_header = self.get_oauth_header(parameters)
            body_parameters = self.body_params(parameters)
        else:
            body_parameters = urlencode({'oauth_token' : parameters['oauth_token']})
            oauth_header = {}
         
            
        return body_parameters, oauth_header

    def get_required_oauth_params(self):
        """
        Returns a dict with the bare minimum of oauth parameters required.
        
          * oauth_consumer_key is simply fetched
          * oauth_signature_method is always HMAC-SHA1
          * oauth_version is always 1.0
          * oauth_timestamp will be time.time()
          * oauth_nonce is a longish int
          
        @return: A dict with oauth_parameters
        @returntype: {}
        """
        from random import getrandbits
        from time import time
        
        return {'oauth_consumer_key': self.service_info['consumer_key'],
                'oauth_signature_method': 'HMAC-SHA1',
                'oauth_version': '1.0',
                'oauth_timestamp': int(time()),
                'oauth_nonce': getrandbits(20)}
        
    def get_params(self, extra_parameters, token):
        """
        Gathers all parameters, both request specific and token related and returns them in a dict.
        
        @param extra_parameters: A dict with parameters to be sent with the request
        @type extra_parameters: dict
        
        @param token: A request or access token
        @type token: oauth.tokens.OAuthRequestToken | oauth.tokens.OAuthAccessToken
        
        @return: A dict with all the parameters.
        @returntype: dict
        
        @note: Does not include oauth_signature which needs to be created outside of the scope
        of this method.
        """
        
        params = self.get_required_oauth_params()
        params.update(extra_parameters)
        
        if token is not None:
            params['oauth_token'] = token.oauth_token
            if hasattr(token, 'oauth_verifier') and token.oauth_verifier is not None:
                    params['oauth_verifier'] = token.oauth_verifier
        
        return params
    
    def construct_signature_key(self, token, parameters):
        """
        Generates the oauth signature key needed for the current request.
        
        @param token: A request or accesstoken as defined in oauth.tokens
        @type token: oauth.tokens.OAuthRequestToken | oauth.tokens.OAuthAccessToken
        
        @param parameters: the parameters to go out with the request
        @type parameters: {}
        
        @return: A string on the format OAUTH_CONSUMER_SECRET&OAUTH_TOKEN_SECRET
        @returntype: String
        
        """
        from oauth.utils import encode
        from oauth.utils import get_service_key
        
        if self.service_key is None: #Consumer secret
            self.service_key = get_service_key(self.service)
        
        key = self.service_key

        if token is not None:
            key = self.service_key + encode(token.oauth_token_secret)
            
        return key
    
    def construct_signature(self, url, method, parameters, token):
        """
        Returns the signature string which signs the current request.
        
        @param url: The resource locator for the called API
        @type url: str
        
        @param meth: The http request method, either "GET" or "POST"
        @type meth: str
        
        @param parameters: the parameters to go out with the request
        @type parameters: {}
        
        @param token: A request or access token
        @type token: oauth.tokens.OAuthRequestToken | oauth.token.OAuthAccessToken
        
        @return: The signature string
        @returntype: str
        """
        from hmac import new as hmac
        from hashlib import sha1
        
        key = self.construct_signature_key(token, parameters)
        
        signature_base_string = self.construct_signature_base_string(url, method, parameters)
        
        return hmac(key, signature_base_string, sha1).digest().encode('base64')[:-1]
    
    def construct_signature_base_string(self, url, method, parameters):
        """
        Constructs the signature base string for a request.
        
        @param url: The resource locator for the called API
        @type url: str
        
        @param method: The http request method, either "GET" or "POST"
        @type method: str
        
        @param parameters: the parameters to go out with the request
        @type parameters: {}
        
        @return: The signature base string
        @returntype: str        
        """
        from oauth.utils import encode
        
        return '&'.join(map(
                            encode,
                            [method.upper(),
                             url,
                             '&'.join('%s=%s' % (encode(k), encode(parameters[k])) for k in sorted(parameters))]
                            )
                        )
    
    def get_oauth_header(self, parameters):
        """
        Constructs a string which is supposed to be used in the outgoing requests's
        Authorization header.
        
        @param parameters: the parameters to go out with the request
        @type parameters: {}
        
        @return: A string which can be used in the Authorization header
        @returntype: str
        """
        from oauth.utils import encode
        
        oauth_header = "OAuth "
        
        for key, value in parameters.iteritems():
            if key.startswith('oauth_'):
                key = encode(key)
                value = encode(value)
                oauth_header += """%s="%s",""" % (key, value)                
        
        oauth_header = oauth_header[:-1]
        
        return oauth_header
    
    def body_params(self, parameters):
        """
        Goes through a list of parameters, and removes any oauth parameters.
        
        It returns a urlencoded parameter list, to be used either as the body of a POST
        request or suffix of a GET request.
        
        @param parameters: A dict with parameters
        @type parameters: dict
        
        @return: a urlencoded parameter list, as a string
        @returntype: str
        """
        from urllib import urlencode
        
        body_params = {}
        
        for key, value in parameters.iteritems():
            if not key.startswith('oauth_'):
                body_params[key] = value
                
        return urlencode(body_params)
    
    def get_cookie(self):
        return SessionCookie.get_cookie("oauth.%s" %self.service, self.handler)
    
    def set_cookie(self, value):
        SessionCookie.set_cookie("oauth.%s" % self.service, value, self.handler)
        
    def expire_cookie(self):
        SessionCookie.expire_cookie(self.handler)
        
    def callback(self, return_to='/'):
        from oauth.tokens import OAuthRequestToken
        
        oauth_token = self.handler.request.get("oauth_token")
        
        if not oauth_token:
            return self.get_request_token()

        oauth_token = OAuthRequestToken.all().filter(
            'oauth_token =', oauth_token).filter(
            'service =', self.service).fetch(1)[0]
        
        if self.handler.request.get("oauth_verifier"):
            oauth_verifier = self.handler.request.get("oauth_verifier")
            oauth_token.oauth_verifier = oauth_verifier
            oauth_token.put()
        
        token_info = self.get_data_from_signed_url(
            self.service_info['access_token_url'], oauth_token
            )
        
        self.persist_access_token(token_info)

        self.handler.redirect(return_to)
    
    def persist_access_token(self, token_info):
        """
        Saves the access token to the datastore, and also connects it to a cookie.
        
        @param token_info: This is the response from the access_token step.
        @type token_info: str
        """
        from oauth.tokens import OAuthAccessToken
        from oauth.utils import create_uuid
        
        key_name = create_uuid()
        
        token_info = token_info.strip()
        
        self.token = OAuthAccessToken(
            key_name=key_name, service=self.service,
            **dict(token.split('=') for token in token_info.split('&'))
            )

        self.token.put()
        self.set_cookie(key_name)
        
    @staticmethod
    def cleanup():
        """
        Deletes request CLEANUP_BATCH_SIZE tokens older than EXPIRATION_WINDOW
        from the data store.
        """
        from oauth.config import CLEANUP_BATCH_SIZE, EXPIRATION_WINDOW
        from oauth.tokens import OAuthRequestToken
        from datetime import datetime
        
        query = OAuthRequestToken.all().filter(
            'created <', datetime.now() - EXPIRATION_WINDOW
            )
        count = query.count(CLEANUP_BATCH_SIZE)
        db.delete(query.fetch(CLEANUP_BATCH_SIZE))
        return "Cleaned %i entries" % count
    
    def login(self):
        SessionCookie.expire_cookie(self.handler)
        
        return self.get_request_token()

    def logout(self, return_to='/'):
        SessionCookie.expire_cookie(self.handler)
        self.handler.redirect(self.handler.request.get("return_to", return_to))